/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of SimpleEL.
 * 
 * SimpleEL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * SimpleEL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with SimpleEL. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.simpleel.internal;

import java.lang.reflect.*;
import java.util.*;
import ch.simpleel.*;
import ch.simpleel.accessors.*;

/**
 * Helper evaluating simple EL expressions on a beans.<br>
 * Currently the following features were supported (also in combination):<br>
 * - Nested properties (<code>address.city.code</code>)<br>
 * - Array/listelements (<code>address[2]</code>)<br>
 * - Mapelements identified by their key (<code>members['john']</code>)<br>
 * 
 * @author M. Hautle
 */
public class ELUtils {
    /** Mask for an '.' control character. */
    private static final int POINT = 0x1;

    /** Mask for an '[' control character. */
    private static final int OPEN_BRACKET = 0x2;

    /** Mask for an ordinary character. */
    private static final int CHAR = 0x4;

    /** Object representing an undefined value. */
    static final Object UNDEFINED = new Object();

    // TODO support expressions starting with an index

    /**
     * Hidden constructor.
     */
    private ELUtils() {
    }

    /**
     * Returns the value of the passed expression.
     * 
     * @param <T> The return type
     * @param el The expression
     * @param bean The bean on which the expression gets executed
     * @param undefined The object to return if the result is undefined
     * @param checked True if the passed EL expression should be checked using {@link #isValid(String)}
     * @return The value of the passed expression on the given bean
     * @throws ValueAccessException If the access failed
     */
    public static <T> T getValueOf(String el, Object bean, T undefined, boolean checked) throws ValueAccessException {
        if (checked)
            checkEL(el);
        @SuppressWarnings("unchecked")
        final T value = (T) getValueOfInternal(el, bean);
        return value == UNDEFINED ? undefined : value;
    }

    /**
     * Returns the value of the passed expression. <br>{@link #UNDEFINED} is returned if the expression couldn't be evaluated (cause there were somwhere null
     * values)
     * 
     * @param el The expression
     * @param bean The bean on which the expression gets executed
     * @return The value of the passed expression on the given bean or {@link #UNDEFINED}
     * @throws ValueAccessException If the access failed
     */
    private static Object getValueOfInternal(String el, Object bean) throws ValueAccessException {
        try {
            // flag indicating if the current 'atom' is an simple property
            boolean prop = true;
            for (int i = 0, end = el.length() - 1; bean != null && i < end;) {
                final String ex = el.substring(i, i = getEndIndex(el, i));
                // call the corresponding resolver (simple property/'indexed' property)
                if (prop) {
                    bean = getPropertyValue(ex, bean);
                } else {
                    bean = getIndexedValue(ex, bean);
                    if (bean == UNDEFINED)
                        return UNDEFINED;
                }
                // last element?
                if (i >= end)
                    return bean;
                // if the end index is a . then the next atom will be a property
                // skip following '.' (may occour after a index -> a[2].b and in this case the following element is a property)
                if (prop = el.charAt(i) == '.' || el.charAt(++i) == '.')
                    i++;
            }
            return UNDEFINED;
        } catch (Exception e) {
            throw new ValueAccessException(el, e);
        }
    }

    /**
     * Sets the value on the passed bean using the expression to figure out the right location.
     * 
     * @param el The expression
     * @param bean The bean on which the expression gets executed or null
     * @param value The value to set (may be null)
     * @param checked True if the passed EL expression should be checked using {@link #isValid(String)}
     * @return True if something was copied
     * @throws ValueAccessException If the access failed
     */
    public static boolean setValueOf(String el, Object bean, Object value, boolean checked) throws ValueAccessException {
        if (checked)
            ELUtils.checkEL(el);
        try {
            // flag indicating if the current 'atom' is an simple property
            boolean prop = true;
            for (int i = 0, cnt = el.length() - 1; bean != null && i < cnt;) {
                final String ex = el.substring(i, i = getEndIndex(el, i));
                // if this is the last expression part we write to it instead of reading it
                if (i >= cnt)
                    return setValue(ex, bean, value);
                // call the corresponding resolver (simple property/'indexed' property)
                if (prop) {
                    bean = getPropertyValue(ex, bean);
                } else {
                    bean = getIndexedValue(ex, bean);
                    if (bean == ELUtils.UNDEFINED)
                        return false;
                }
                // if the end index is a . then the next atom will be a property
                // skip following '.' (may occour after a index -> a[2].b and in this case the following element is a property)
                if (prop = el.charAt(i) == '.' || el.charAt(++i) == '.')
                    i++;
            }
            return false;
        } catch (Exception e) {
            throw new ValueAccessException(el, e);
        }
    }

    /**
     * Checks wherever the given expression is just an property access (so that it calls only a getter/setter).
     * 
     * @param el The expression
     * @return True if it's just a flat property access
     */
    public static boolean isSimpleProperty(String el) {
        return getEndIndex(el, 0) == el.length();
    }

    /**
     * Returns the value.
     * 
     * @param identifier The the index or the map key
     * @param element The element on which the operation should be executed
     * @return The resulting value or the {@link #UNDEFINED} for inexistent indices
     */
    static Object getIndexedValue(String identifier, Object element) {
        // list
        if (element instanceof List) {
            final List list = (List) element;
            final int index = Integer.parseInt(identifier);
            if (index < list.size())
                return list.get(index);
            return UNDEFINED;
        }
        // map
        if (element instanceof Map) {
            final char c = identifier.charAt(0);
            if (c == '\'' || c == '"')
                identifier = identifier.substring(1, identifier.length() - 1);
            return ((Map) element).get(identifier);
        }
        // array
        if (element.getClass().isArray()) {
            final int index = Integer.parseInt(identifier);
            if (index < Array.getLength(element))
                return Array.get(element, index);
            return UNDEFINED;
        }
        return UNDEFINED;
    }

    /**
     * Returns the value.
     * 
     * @param identifier The property name
     * @param element The element on which the operation should be executed
     * @return The resulting value
     * @throws Exception The unwrapped cause of an {@link ValueAccessException}
     */
    static Object getPropertyValue(String identifier, Object element) throws Exception {
        try {
            return ValueAccessorManager.getValue(element, identifier, null);
        } catch (ValueAccessException e) {
            throw e.getCause();
        }
    }

    /**
     * Sets the value.
     * 
     * @param identifier The property name, the index or the map key
     * @param element The element on which the operation should be executed
     * @param value The value to set
     * @return True if the value was set
     * @throws Exception The unwrapped cause of an {@link ValueAccessException}
     */
    @SuppressWarnings("unchecked")
    static boolean setValue(String identifier, Object element, Object value) throws Exception {
        // list
        if (element instanceof List) {
            final List list = (List) element;
            final int index = Integer.parseInt(identifier);
            if (index >= list.size())
                return false;
            list.add(index, value);
            return true;
        }
        // map
        if (element instanceof Map) {
            final char c = identifier.charAt(0);
            if (c == '\'' || c == '"')
                identifier = identifier.substring(1, identifier.length() - 1);
            ((Map) element).put(identifier, value);
            return true;
        }
        // array
        if (element.getClass().isArray()) {
            final int index = Integer.parseInt(identifier);
            if (index >= Array.getLength(element))
                return false;
            Array.set(element, index, value);
            return true;
        }
        // bean
        try {
            return ValueAccessorManager.setValue(element, identifier, value);
        } catch (ValueAccessException e) {
            throw e.getCause();
        }
    }

    /**
     * Returns the end index (exclusive) of the expression starting at <code>index</code>.
     * 
     * @param el The complete expression
     * @param index The starting index (inclusive)
     * @return The end index (exclusive) of the expression starting at <code>index</code>
     */
    static int getEndIndex(String el, int index) {
        final int cnt = el.length();
        for (int i = index; i < cnt; i++) {
            switch (el.charAt(i)) {
                case '.':
                case '[':
                case ']':
                    return i;
                    // skip strings
                case '\'':
                case '"':
                    final char c = el.charAt(i);
                    for (i++; i < cnt && el.charAt(i) != c; i++)
                        ;
            }
        }
        return cnt;
    }

    /**
     * Ensures that the given expression is valid, if not a {@link IllegalArgumentException} will be thrown.
     * 
     * @param el The expression to check
     * @throws IllegalArgumentException If the EL expression is not valid.
     */
    public static void checkEL(String el) throws IllegalArgumentException {
        if (!isValid(el))
            throw new IllegalArgumentException("\"" + el + "\" is not a valid input sequence!");
    }

    /**
     * Checks the given expression on syntactical correctness.
     * 
     * @param el The expression to check
     * @return True if the expression is valid
     */
    public static boolean isValid(String el) {
        // a el expression must start with an identifier part
        if (el.length() == 0 || !Character.isJavaIdentifierPart(el.charAt(0)))
            return false;
        boolean valid = true;
        // mask defining the next allowed control character(s)
        int mask = CHAR | POINT | OPEN_BRACKET;
        // check the expression
        for (int i = 1, cnt = el.length(); valid && i < cnt; i++) {
            final char c = el.charAt(i);
            switch (c) {
                case '.':
                    valid = (mask & POINT) != 0;
                    mask = CHAR;
                    break;
                case '[':
                    if ((mask & OPEN_BRACKET) == 0)
                        return false;
                    i = getEndBracket(el, i);
                    valid = i != -1;
                    mask = POINT | OPEN_BRACKET;
                    break;
                default:
                    valid = (mask & CHAR) == CHAR && Character.isJavaIdentifierPart(c);
                    mask = CHAR | POINT | OPEN_BRACKET;
            }
        }
        return valid && mask != CHAR;
    }

    /**
     * Returns the index of the ending bracket ']'.
     * 
     * @param el The expression
     * @param i The index of the starting bracket '['
     * @return The index of the end bracket or -1 if the bracket part is invalid
     */
    private static int getEndBracket(String el, int i) {
        i++;
        for (int cnt = el.length(); i < cnt; i++) {
            final char c = el.charAt(i);
            switch (c) {
                case ']':
                    return i;
                case '[':
                    return -1;
                    // skip strings
                case '\'':
                case '"':
                    for (i++; i < cnt && el.charAt(i) != c; i++)
                        ;
            }
        }
        return -1;
    }
}
